/**
 * DerwentBridgeSource is the feeder for hydro sensors located at Derwent
 * Bridge. This feeder iterates through each valid file specified by the
 * path provided by the delegate configuration, parses those files and 
 * dispatches all valid samples. 
 * 
 * External packages and classes can be referenced in 3 different ways.
 * - by fully qualified name, e.g. Packages.au.csiro.ict.tasman.core.Message
 * - by global import using importClass() and importPackage(). These are
 * globally accessible within the script.
 * - by scoped import using JavaImporter(). Code access these scoped packages
 * and classes are enabled by surrounding them in a with(){ ... } block. e.g. 
 * with(JodaTimeClasses){ ... }
 * 
 * Javascript has no native threading so there is a lot of cool hackery
 * going on in this script using Java threads and concurrency.
 * 
 * @author Ben Howell <ben.howell@csiro.au>
 */


// Global imports
importClass(java.lang.Thread);
importClass(java.lang.Runnable);
importPackage(java.util.concurrent.locks);
importClass(Packages.au.csiro.ict.tasman.core.Message);
importClass(Packages.au.csiro.ict.tasman.util.QuickCache);
importPackage(Packages.au.csiro.ict.tasman.datatype);


// Scoped imports
var JodaTimeClasses = new JavaImporter(
  org.joda.time.DateTime,
  org.joda.time.DateTimeZone,
  org.joda.time.format.DateTimeFormat
);


// Scoped imports
var JavaFileClasses = new JavaImporter(
  java.io.BufferedReader,
  java.io.FileReader,
  java.io.File
);


/**
 * Called by au.csiro.ict.tasman.source.ScriptSource. This is the entry point
 * for this script.
 * @return the running script instance.
 */
function run(){
  return new DerwentBridgeSource();
}


/**
 * Called by au.csiro.ict.tasman.source.ScriptSource.
 * If false, SourceManager will clean up calling thread and attempt a restart.
 * on the Java side.
 * @return true if running, else false.
 */
function isRunning(){
  var self = instance;
  if(self != null) 
    return self.thread.isAlive();
  else
    return false;
}


/**
 * Called by au.csiro.ict.tasman.source.ScriptSource. This allows script to 
 * perform its own cleanup routine before exiting.
 * @return true after shutdown.
 */
function shutDown(){
  var self = instance;
  source.getLogger().debug("shutting down script...");
  self.threadCancelled = true;
  source.getLogger().debug("forcibly waking thread...");
  self.sleeper.waken();
  //block while waiting for thread to terminate
  while (self.thread.isAlive()){
    try {
      Thread.sleep(1000);
    } 
    catch (e) {
      source.getLogger().error("Unexpected error");
      self.threadCancelled = true;
    }
  }
  source.getLogger().debug("script terminated!");
  self.cache.save();
  return true;
}


/**
 * An interruptable sleep routine.
 */
function Sleeper(){
  var self = this;
  this.lock = new ReentrantLock();
  this.wake = this.lock.newCondition();

  
  /**
   * Starts a thread containing a sleep routine.
   * @param interval the sleep interval in seconds
   */
  this.sleep = function(interval){
    self.thread = new Thread(new Runnable(){run: self.sleeper(self, interval)});
    self.thread.start();
    self.lock.lock();
    self.wake.await();
    self.lock.unlock();
  };
  
  
  /**
   * Interruptable sleep thread.
   * @param self a reference to our containing self who spawned this thread
   * routine (i.e. Sleeper().this).
   * @param interval the sleep interval in seconds
   * @return the inner function declaration.
   */
  this.sleeper = function(self, interval){
    function inner(){
      try {
	Thread.sleep(interval * 1000);
      } 
      catch (e) {
	var je = e.javaException;
	if (!(je instanceof java.lang.InterruptedException)) {
	  source.getLogger().warn("Unexpected error " + je);
	}
      }
      finally{
	self.lock.lock();
	self.wake.signalAll();
	self.lock.unlock();
      }
    }
    return inner;
  };
  
  
  /**
   * Interrupts the sleep thread. This breaks sleep without waiting for sleep
   * interval to complete.
   */
  this.waken = function(){
    self.thread.interrupt();
  };
};


/**
 * Constructor.
 */
function DerwentBridgeSource(){
  var self = this;
  self.sleeper = new Sleeper();
  this.threadCancelled = false;
  
  //set configuration items
  this.configuration = JSON.parse(source.getConfiguration().getConfiguration().toString());
  this.fileNamePattern = new RegExp(this.configuration.filenamepattern);
  this.rowPattern = new RegExp(this.configuration.rowpattern);
  this.interval = this.configuration.interval;
  this.fileDir = source.getDataDirectory() + this.configuration.path;
  this.cache = new QuickCache(this.fileDir + "cache.json");
  with(JodaTimeClasses){
    this.datetimeFormat = DateTimeFormat.forPattern(this.configuration.timestamp);
    this.datetimeFormat = this.datetimeFormat.withZone(DateTimeZone.UTC);
  }
  
  this.thread = new Thread(new Runnable(){run: main(self)});
  this.thread.start();
  
  source.getLogger().info("DerwentBridgeSource initialised");
};


/**
 * Main loop.
 * @param self a reference to our containing self who spawned this thread
 * routine (i.e. DerwentBridgeSource().this).
 * @return the inner function declaration.
 */
function main(self){
  function inner(){
    while(!self.threadCancelled){
      //javascript has no native file I/O, let's use Java... 
      with(JavaFileClasses){
	var directory = new File(self.fileDir);
	var files = directory.list();
      }
      for (i=0;i<files.length;i++){
	var match = self.fileNamePattern.exec(files[i]);
	if (match != null && !self.cache.contains(files[i])){
	  self.cache.add(files[i]);
	  var sensorId = buildSensorId(match);
	  var messages = parseFile(self, files[i], sensorId);
	  source.dispatch(messages);
	}
      }
      self.cache.save();
      source.getLogger().debug("sleeping...");
      self.sleeper.sleep(self.interval);
      source.getLogger().debug("waking...");
    }
  }
  return inner;
}


/**
 * Parses each line in the file given, creates samples and
 * returns a message collection containing those samples.
 * @param self a reference to our containing self who called this
 * routine (i.e. DerwentBridgeSource().this).
 * @param file name of the file to parse.
 * @param sensorId the sensor id of the observations in the file.
 * @return Array of messages.
 */
function parseFile(self, file, sensorId){
  var messages = new Array();
  try {
    //javascript has no native file I/O, let's use Java...
    with(JavaFileClasses){
      var reader = new BufferedReader(new FileReader(self.fileDir + file));
      var line;
      source.getLogger().debug("parsing file: " + file);
      while ((line = reader.readLine()) != null) {
	var match = self.rowPattern.exec(line);
	if (match != null){
	  if(parseInt(match[4]) < 151){
	    // Create Sample...
	    var s = new Sample();
	    with(JodaTimeClasses)
	      s.time = self.datetimeFormat.parseDateTime(match[1] + match[2]);
	    s.value = parseFloat(match[3]);
	    s.sensorId = sensorId;
	    var annotation = match[6].replace('\r', '');
	    if (annotation != "")
	      s.tags.put("annotation", annotation);
	    messages.push(new Message(s.sensorId, s));
	  }
	  else{
	    source.getLogger().debug("UNUSABLE DATA: " + match[0]);
	  }
	}
      }
      reader.close();
    }
  }
  catch(e){
    source.getLogger().error("I/O exception whilst parsing file: " + file);
    self.cache.remove(file);
  }
  return messages;
};


/**
 * Returns a sensor id (network.feature.sensor) using parts from the name of
 * the file which contains the sensors observations.
 * @param match the regex match group from the name of the file containing the
 * observations.
 * @return a sensor id (network.platform.sensor)
 */
function buildSensorId(match){
  var sensorId = null;
  if(match[4] == 'EC')
    sensorId = 'conductivity';
  else if(match[4] == 'T')
    sensorId = 'temperature';
  else if(match[4] == 'DO')
    sensorId = 'dissolved_oxygen';
  else if(match[4] == 'DOpercent')
    sensorId = 'oxygen_saturation';
  else if(match[4] == 'SAL')
    sensorId = 'salinity';
  if(sensorId != null)
    return "hydro.derwent_bridge." + sensorId + "_" + match[2] + 'm';
  return sensorId;
};
